﻿namespace Nomo.UnityCoreModule
{
    /// <summary>
    /// 计时器。
    /// </summary>
    public sealed class Timer : UnityEngine.MonoBehaviour
    {
        /// <summary>
        /// 计时器快照。用于记录计时器在某一特定时刻的状态。
        /// </summary>
        [System.Serializable]
        public struct TimerSnapshot
        {
            /// <summary>
            /// 计时器是否正在计时？
            /// </summary>
            public bool timing;

            /// <summary>
            /// 计时器是否使用真实时间来计时？
            /// </summary>
            public bool realtime;

            /// <summary>
            /// 计时器的循环次数。
            /// </summary>
            public int loopTime;

            /// <summary>
            /// 计时器的计时起点。
            /// </summary>
            public float from;

            /// <summary>
            /// 计时器的计时终点。
            /// </summary>
            public float to;

            /// <summary>
            /// 计时器的当前时间。
            /// </summary>
            public float time;

            /// <summary>
            /// 计时器的计时进度。
            /// </summary>
            public float progress;

            /// <summary>
            /// 计时器的精确到秒的当前时间。
            /// </summary>
            public float second;
        }

        /// <summary>
        /// 引发计时器与属性改变相关的事件的原因。
        /// </summary>
        public enum TimerValueChangedCausation
        {
            /// <summary>
            /// 因正常计时而引发事件。
            /// </summary>
            Time,

            /// <summary>
            /// 因修改属性而引发事件。
            /// </summary>
            Modify
        }

        /// <summary>
        /// 计时器结束事件。参数为这个计时器本身。
        /// </summary>
        [System.Serializable]
        public sealed class TimerCompletedEvent : UnityEngine.Events.UnityEvent<Timer> { }

        /// <summary>
        /// 计时器数值改变事件。参数依次为这个计时器本身、引发此事件的原因、当前值、当前值与上一次的值的差值。
        /// </summary>
        [System.Serializable]
        public sealed class TimerValueChangedEvent : UnityEngine.Events.UnityEvent<Timer, TimerValueChangedCausation, float, float> { }

        /// <summary>
        /// 正在计时吗？
        /// </summary>
        public bool timing;

        /// <summary>
        /// 是否使用真实时间来计时？
        /// </summary>
        public bool realtime;

        /// <summary>
        /// 循环次数。
        /// </summary>
        public int loopTime;

        [UnityEngine.SerializeField] private float                  from;
        [UnityEngine.SerializeField] private float                  to;
        [UnityEngine.SerializeField] private float                  time;
        [UnityEngine.SerializeField] private float                  progress        = 1F;
        [UnityEngine.SerializeField] private TimerCompletedEvent    completed       = new TimerCompletedEvent();
        [UnityEngine.SerializeField] private TimerValueChangedEvent timeChanged     = new TimerValueChangedEvent();
        [UnityEngine.SerializeField] private TimerValueChangedEvent secondChanged   = new TimerValueChangedEvent();
        [UnityEngine.SerializeField] private TimerValueChangedEvent progressChanged = new TimerValueChangedEvent();

        /// <summary>
        /// 获取或设置计时起点。
        /// </summary>
        /// <exception cref="System.OverflowException"><paramref name="value"/> 为 <see cref="float.PositiveInfinity"/> 或 <see cref="float.NegativeInfinity"/> .</exception>
        /// <exception cref="System.ArgumentException"><paramref name="value"/> 为 <see cref="float.NaN"/> .</exception>
        public float From { get => from; set => SetFrom(TimerValueChangedCausation.Modify, value); }

        /// <summary>
        /// 获取或设置计时终点。
        /// </summary>
        /// <exception cref="System.ArgumentException"><paramref name="value"/> 为 <see cref="float.NaN"/> .</exception>
        public float To { get => to; set => SetTo(TimerValueChangedCausation.Modify, value); }

        /// <summary>
        /// 获取或设置当前时间。
        /// </summary>
        /// <exception cref="System.ArgumentException"><paramref name="value"/> 为 <see cref="float.NaN"/> .</exception>
        public float Time { get => time; set => SetTime(TimerValueChangedCausation.Modify, value); }

        /// <summary>
        /// 获取精确到秒的当前时间。
        /// </summary>
        public float Second { get; private set; }

        /// <summary>
        /// 获取或设置计时进度。
        /// </summary>
        /// <exception cref="System.OverflowException"><paramref name="value"/> 小于 0 或大于 1 .</exception>
        /// <exception cref="System.ArgumentException"><paramref name="value"/> 为 <see cref="float.NaN"/> .</exception>
        public float Progress { get => progress; set => SetProgress(TimerValueChangedCausation.Modify, value); }

        /// <summary>
        /// 计时结束。
        /// </summary>
        public TimerCompletedEvent Completed => completed;

        /// <summary>
        /// 当前时间改变。
        /// </summary>
        public TimerValueChangedEvent TimeChanged => timeChanged;

        /// <summary>
        /// 精确到秒的当前时间改变。
        /// </summary>
        public TimerValueChangedEvent SecondChanged => secondChanged;

        /// <summary>
        /// 计时进度改变。
        /// </summary>
        public TimerValueChangedEvent ProgressChanged => progressChanged;

        private void SetFrom(TimerValueChangedCausation causation, float value)
        {
            if (float.IsNaN(value))
            {
                throw new System.ArgumentException("不允许设置为非数字。", nameof(value));
            }
            if (float.IsInfinity(value))
            {
                throw new System.OverflowException("不允许设置为无穷。");
            }
            if (value == from)
            {
                return;
            }
            from = value;
            var oldTime = time;
            time = Clamp(time, from, to);
            if (time != oldTime)
            {
                OnTimeChanged(causation, time, time - oldTime);
                var oldSecond = Second;
                Second = (float) System.Math.Truncate(time);
                if (Second != oldSecond)
                {
                    OnSecondChanged(causation, Second, Second - oldSecond);
                }
            }
            var oldProgress = progress;
            progress = InverseLerp(time, from, to);
            if (progress != oldProgress)
            {
                OnProgressChanged(causation, progress, progress - oldProgress);
            }
        }

        private void SetTo(TimerValueChangedCausation causation, float value)
        {
            if (float.IsNaN(value))
            {
                throw new System.ArgumentException("不允许设置为非数字。", nameof(value));
            }
            if (value == to)
            {
                return;
            }
            to = value;
            var oldTime = time;
            time = Clamp(time, from, to);
            if (time != oldTime)
            {
                OnTimeChanged(causation, time, time - oldTime);
                var oldSecond = Second;
                Second = (float) System.Math.Truncate(time);
                if (Second != oldSecond)
                {
                    OnSecondChanged(causation, Second, Second - oldSecond);
                }
            }
            var oldProgress = progress;
            progress = InverseLerp(time, from, to);
            if (progress != oldProgress)
            {
                OnProgressChanged(causation, progress, progress - oldProgress);
            }
        }

        private void SetTime(TimerValueChangedCausation causation, float value)
        {
            if (float.IsNaN(value))
            {
                throw new System.ArgumentException("不允许设置为非数字。", nameof(value));
            }
            if (value == time)
            {
                return;
            }
            var oldTime = time;
            time = Clamp(value, from, to);
            if (time != oldTime)
            {
                OnTimeChanged(causation, time, time - oldTime);
                var oldSecond = Second;
                Second = (float) System.Math.Truncate(time);
                if (Second != oldSecond)
                {
                    OnSecondChanged(causation, Second, Second - oldSecond);
                }
                var oldProgress = progress;
                progress = InverseLerp(time, from, to);
                if (progress != oldProgress)
                {
                    OnProgressChanged(causation, progress, progress - oldProgress);
                }
            }
        }

        private void SetProgress(TimerValueChangedCausation causation, float value)
        {
            if (float.IsNaN(value))
            {
                throw new System.ArgumentException("不允许设置为非数字。", nameof(value));
            }
            if (value < 0F || value > 1F)
            {
                throw new System.OverflowException("不允许设置为小于 0 或大于 1 的值。");
            }
            if (value == progress)
            {
                return;
            }
            var oldProgress = progress;
            var oldTime     = time;
            if (!float.IsInfinity(to))
            {
                time = Lerp(from, to, value);
            }
            else
            {
                time = value == 0F ? from : to;
            }
            if (time != oldTime)
            {
                OnTimeChanged(causation, time, time - oldTime);
                var oldSecond = Second;
                Second = (float) System.Math.Truncate(time);
                if (Second != oldSecond)
                {
                    OnSecondChanged(causation, Second, Second - oldSecond);
                }
            }
            progress = InverseLerp(time, from, to);
            if (progress != oldProgress)
            {
                OnProgressChanged(causation, progress, progress - oldProgress);
            }
        }

        /// <summary>
        /// 快照。
        /// </summary>
        public TimerSnapshot Snapshot()
        {
            return new TimerSnapshot
            {
                timing   = timing,
                realtime = realtime,
                loopTime = loopTime,
                from     = from,
                to       = to,
                time     = time,
                progress = progress,
                second   = Second
            };
        }

        private void OnCompleted()
        {
            completed.Invoke(this);
        }

        private void OnTimeChanged(TimerValueChangedCausation causation, float value, float delta)
        {
            timeChanged.Invoke(this, causation, value, delta);
        }

        private void OnSecondChanged(TimerValueChangedCausation causation, float value, float delta)
        {
            secondChanged.Invoke(this, causation, value, delta);
        }

        private void OnProgressChanged(TimerValueChangedCausation causation, float value, float delta)
        {
            progressChanged.Invoke(this, causation, value, delta);
        }

        private static float Clamp(float value, float a, float b)
        {
            return a < b ? UnityEngine.Mathf.Clamp(value, a, b) : a > b ? UnityEngine.Mathf.Clamp(value, b, a) : a;
        }

        private static float InverseLerp(float value, float a, float b)
        {
            if (value == b || a == b)
            {
                return 1F;
            }
            if (value == a)
            {
                return 0F;
            }
            return (float) (((double) value - a) / ((double) b - a));
        }

        private static float Lerp(float a, float b, float t)
        {
            return UnityEngine.Mathf.LerpUnclamped(a, b, t);
        }

        /// <summary>
        /// 返回一个字符串，它表示精确到小数点后两位的当前时间。
        /// </summary>
        /// <returns>表示精确到小数点后两位的当前时间的字符串。</returns>
        public override string ToString()
        {
            return time.ToString("F2", System.Globalization.NumberFormatInfo.CurrentInfo);
        }

        private void Update()
        {
            if (!timing)
            {
                return;
            }
            var oldTime = time;
            time = UnityEngine.Mathf.MoveTowards(time, to, !realtime ? UnityEngine.Time.deltaTime : UnityEngine.Time.unscaledDeltaTime);
            if (time != oldTime)
            {
                OnTimeChanged(TimerValueChangedCausation.Time, time, time - oldTime);
                var oldSecond = Second;
                Second = (float) System.Math.Truncate(time);
                if (Second != oldSecond)
                {
                    OnSecondChanged(TimerValueChangedCausation.Time, Second, Second - oldSecond);
                }
                var oldProgress = progress;
                progress = InverseLerp(time, from, to);
                if (progress != oldProgress)
                {
                    OnProgressChanged(TimerValueChangedCausation.Time, progress, progress - oldProgress);
                }
            }
            if (time != to)
            {
                return;
            }
            timing = false;
            OnCompleted();
            if (timing)
            {
                UnityEngine.Debug.LogError($"不允许在 {nameof(Completed)} 中将 {timing} 设置为 {true} ，想要循环计时，请使用 {nameof(loopTime)} 来控制。");
                timing = false;
            }
            if (loopTime != 0)
            {
                if (loopTime > 0)
                {
                    --loopTime;
                }
                SetTime(TimerValueChangedCausation.Time, from);
                timing = true;
            }
        }
    }
}
